﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Com.Net.Utility;

namespace Com.Net.Sockets
{
    public class AsyncSocketPool
    {
        private const int OpsToPreAlloc = 2;
        private readonly BufferManager mBufferManager;
        private readonly Semaphore mMaxNumberConnections;
        private readonly int mNumConnections;
        private readonly SocketAsyncEventArgsPool mEArgsPool;
        private int mNumConnectedSockets;
        private int mReceiveBufferSize;
        private int mTotalBytesRead;

        public AsyncSocketPool(int numConnections, int receiveBufferSize)
        {
            mTotalBytesRead = 0;
            mNumConnectedSockets = 0;
            mNumConnections = numConnections;
            mReceiveBufferSize = receiveBufferSize;
            mBufferManager = new BufferManager(receiveBufferSize * numConnections * OpsToPreAlloc, receiveBufferSize);
            mEArgsPool = new SocketAsyncEventArgsPool(numConnections);
            mMaxNumberConnections = new Semaphore(numConnections, numConnections);
        }

        public void Init()
        {
            mBufferManager.InitBuffer();
            SocketAsyncEventArgs readWriteEventArg;
            for (int i = 0; i < mNumConnections; i++)
            {
                readWriteEventArg = new SocketAsyncEventArgs();
                readWriteEventArg.Completed += IoCompleted;
                readWriteEventArg.UserToken = new AsyncUserToken();
                mBufferManager.SetBuffer(readWriteEventArg);
                mEArgsPool.Push(readWriteEventArg);
            }
        }

        public void Connect()
        {
            mMaxNumberConnections.WaitOne();
            SocketAsyncEventArgs eventArgs = mEArgsPool.Pop();
            eventArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 80);
            var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            ((AsyncUserToken)eventArgs.UserToken).Socket = clientSocket;
            bool willRaiseEvent = clientSocket.ConnectAsync(eventArgs);
            if (!willRaiseEvent)
            {
                ProcessConnect(eventArgs);
            }
        }

        private void IoCompleted(object sender, SocketAsyncEventArgs e)
        {
            switch (e.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    ProcessReceive(e);
                    break;
                case SocketAsyncOperation.Send:
                    ProcessSend(e);
                    break;
                case SocketAsyncOperation.Connect:
                    ProcessConnect(e);
                    break;
                default:
                    throw new ArgumentException("The last operation completed on the socket was not a receive or send");
            }
        }

        private void ProcessConnect(SocketAsyncEventArgs args)
        {
            Interlocked.Increment(ref mNumConnectedSockets);
            if (args.SocketError == SocketError.Success)
            {
                var token = (AsyncUserToken)args.UserToken;
                var data = Encoding.ASCII.GetBytes("GET /Login.aspx HTTP/1.1\r\n\r\n");
                Array.Copy(data, 0, args.Buffer, args.Offset, data.Length);
                args.SetBuffer(args.Offset, data.Length);
                token.Socket.SendAsync(args);
            }
            else
            {
                CloseClientSocket(args);
            }
        }

        private void ProcessSend(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                var token = (AsyncUserToken)args.UserToken;
                bool willRaiseEvent = token.Socket.ReceiveAsync(args);
                if (!willRaiseEvent)
                {
                    ProcessReceive(args);
                }
            }
            else
            {
                CloseClientSocket(args);
            }
        }

        private void ProcessReceive(SocketAsyncEventArgs args)
        {
            var token = (AsyncUserToken)args.UserToken;
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                Interlocked.Add(ref mTotalBytesRead, args.BytesTransferred);
                Console.WriteLine("The server has read a total of {0} bytes", mTotalBytesRead);
                bool willRaiseEvent = token.Socket.ReceiveAsync(args);
                if (!willRaiseEvent)
                {
                    ProcessReceive(args);
                }
            }
            else
            {
                CloseClientSocket(args);
            }
        }

        private void CloseClientSocket(SocketAsyncEventArgs e)
        {
            var token = e.UserToken as AsyncUserToken;

            try
            {
                token.Socket.Shutdown(SocketShutdown.Send);
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception.Message);
            }

            token.Socket.Close();
            

            Interlocked.Decrement(ref mNumConnectedSockets);
            mEArgsPool.Push(e);
            mMaxNumberConnections.Release();
            Console.WriteLine(
                "A client has been disconnected from the server. There are {0} clients connected to the server",
                mNumConnectedSockets);
        }
    }
}